/*
 * Copyright (C) 2007  Brunata a/s
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 */

package com.brunata.httprmi;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * 
 * Threadsafe, Immutable
 * @author Casper Bang (cab@brunata.com)
 */
public class HttpRmiProxy implements InvocationHandler
{
    ////////////////////////////////////////////////////////////////////////////
    // Constants
    ////////////////////////////////////////////////////////////////////////////
    
    private static final int    DEFAULT_BUFFER_SIZE    =   4096;
    
    ////////////////////////////////////////////////////////////////////////////
    // Fields
    ////////////////////////////////////////////////////////////////////////////
    
    private IStakeHolder         _stakeHolder = null;
    private URLConnection       _conn = null;
    private String              _endpoint;
    private int                 _bufferSize;
    private volatile boolean    _cancelled = false;
    
    ////////////////////////////////////////////////////////////////////////////
    // Constructors
    ////////////////////////////////////////////////////////////////////////////
    
    /**
     * Constructor, with endpoint and buffersize specifiers. This is not intended
     * to be used directly, but rather let the DynamicProxyFactory create it.
     */ 
    public HttpRmiProxy(String endpoint, IStakeHolder stakeHolder)
    {
        _endpoint = endpoint;
        _stakeHolder = stakeHolder;
        _bufferSize = DEFAULT_BUFFER_SIZE;
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Internals
    ////////////////////////////////////////////////////////////////////////////
    
    /**
     * Helper method that will deserialize a byte array to a <b>java.lang.Object</b>
     */ 
    private final static Object toObject(byte[] result)
    {
        try
        {
            ObjectInputStream ois = new ObjectInputStream( new BufferedInputStream( new ByteArrayInputStream( result) ) );
            Object obj = ois.readObject();
            ois.close();
            return obj;
        }
        catch (ClassNotFoundException ex)
        {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        }
        catch (IOException ex)
        {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        }
        return null;
    }    
    
   
    /**
     * This method is the workhorse of the remoting. It invokes a Servlet by concatenating the endpoint and
     * a method name, with arguments serialized in the POST body.
     * 
     * @throws IOException
     * @throws CancelledException
     */
    private final byte[] invokeRemote(String method, Object... arguments) throws IOException
    {
        InputStream  in = null;
        try
        {
            // Serialize argument array to a byte array
            ByteArrayOutputStream bos = new ByteArrayOutputStream() ;
            ObjectOutput out = new ObjectOutputStream(bos) ;
            out.writeObject(arguments);
            out.flush();
            byte[] payloadBuffer = bos.toByteArray();
            out.close();

            Logger.getLogger("global").log(Level.INFO, "Serialized the arguments into " + payloadBuffer.length + " bytes");
            
            // Open connection, point to desired method and push arguments into data content of POST request
            URL url = new URL(_endpoint + "?ACTION=" + method);
            Logger.getLogger("global").log(Level.INFO, "About to open connection to: " + url);

            _conn = url.openConnection();
            _conn.setUseCaches(true);            
            _conn.setDoOutput(true);
            
            try
            {
                _conn.connect();                
            }
            catch(UnknownHostException ex)
            {
                throw new InvalidEndpointException(ex.getMessage());
            }
            catch(ConnectException ex)
            {
                throw new InvalidEndpointException(ex.getMessage());
            }
            
            // Send HTTP payload with method arguments inside
            DataOutputStream dos = new DataOutputStream(_conn.getOutputStream());
            dos.write(payloadBuffer);
            dos.flush(); 
            dos.close();
            
            try
            {
                in = _conn.getInputStream();
            }
            catch(IOException ex)
            {
                throw new InvalidEndpointException("SOMETHING is there, but the server is not answering in accorance with protocol!");
            }
            
            BufferedInputStream bif = new BufferedInputStream( in, _bufferSize );
            byte[] result = new byte[_conn.getContentLength()];

            int readCount = 0;
            while(readCount != _conn.getContentLength())
            {
                // If call has been cancelled, throw CancelledException to break out
                if(isCancelled())
                    throw new CancelledException("Cancelled after processing " + readCount + " bytes out of " + _conn.getContentLength());
                
                // Read another chunk of the message
                readCount += bif.read(result, readCount, Math.min( _bufferSize, _conn.getContentLength() - readCount ));
                
                // Notify listener of progress
                _stakeHolder.progressChangeEvent( readCount, 0, result.length);
            }
            
            return result;
        }
        finally
        {
            try
            {
                if (in != null)
                    in.close();
            }
            catch (IOException ex)
            {
            }
        }
    }
          
    ////////////////////////////////////////////////////////////////////////////
    // Overrides/implementations
    ////////////////////////////////////////////////////////////////////////////
    
    public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
    {
        Object response = null;
        
        response = toObject( invokeRemote(method.getName(), args ) );
        
        if( response instanceof Exception)
            throw (Exception)response;
        
        return response;
    }

    /*
    public void cancel()
    {
        
        synchronized(this)
        {
            _cancelled = true;
        }
        _listener.isCancelled();
        _l
    }
    */
    
    public boolean isCancelled()
    {/*
        synchronized(this)
        {
            return _cancelled;
        }
     */
        return _stakeHolder.isCancelled();
    }
    
    ////////////////////////////////////////////////////////////////////////////
    // Accessors (getters/setters)
    ////////////////////////////////////////////////////////////////////////////
}


